1 如何停止一个线程?
答:(1)不能简单的停止(Stop())一个线程。因为停止stop()会直接把线程停止,这样就没有给线程足够的时间来处理想要在停止前保存数据的逻辑,任务戛然而止,会导致出现数据完整性等问题;
(2)虽然线程不能在中间被停止/干掉,但是任务是可以停止的;想让线程结束的目的是让任务结束,而不是强制线程结束。有两种方式结束任务,分别是:Interrupt和boolean标志位;
(3)使用线程中断机制-interrupt停止线程,分2种情况。如果原生支持interrupt:sleep、wait等可以让线程进入阻塞的方法使线程休眠了,而处于休眠中的线程被中断,那么线程是可以感受到中断信号的,并且会抛出一个InterruptedException异常,同时清除中断信号,将中断标记位设置成false。非原生如果支持interrupt,每执行一次任务,调用interrupted()或isInterrupted()询问一遍系统是否已经中断了,如果中断了系统会告诉我们已经中断了,然后可以实现中断的逻辑。
(4)其中,interrupt底层的原理是在Native层加锁。然后判断interrupted_值是否等于true,是的直接返回,表示已经是中断状态了;否则,把interrupted_置为true,发出中断通知。实现原理和boolean标志位的实现逻辑很像。
interrupted()与isInterrupted()之间的区别是:在Java层,interrupted()是静态方法,获取当前线程的中断状态后并清空,重复调用后续返回false;isInterrupted()是非静态方法,获取线程对象对应线程的中断状态,不清空,可重复调用,中断清空前一直返回true。在Native层,interrupted()比isInterrupted()多调用了SetInterruptedLocked(false)方法,清空当前中断状态,其他的都一致。
(5)使用volatile boolean标志位停止线程:线程中设置一个boolean标志位值为false,线程里不断读取这个boolean值,其他地方可以修改这个boolean值;为了保证内存可见性,给boolean标志位添加volatile保证可见性;当某一个线程修改boolean标志位为true,线程中能立刻看到。
(6)如何选择interrupt和boolean标志位去停止线程?interrupt()和boolean标志位的原理是一致的。除非是用到了系统方法时(如:sleep) 或者 使用阻塞队列在线程中执行put()时发生阻塞,使用interrupt();否则,建议使用boolean标志位,性能更优,毕竟interrupt使用JNI有一定的开销。。
1.1 这道题想考察什么?
答:(1)考察要点
●是否对线程的用法有了解;是否对线程的stop方法有了解(初级)
●是否对线程stop过程中存在的问题有认识;是否熟悉interrupt中断的用法(中级)
●是否能解释清楚使用boolean标志位的好处;是否知道interrupt底层的细节;通过该题目能够转移话题到线程安全,并阐述无误(高级)
(2)题目剖析
●如何停止一个线程?
●官方停止线程的方法被废弃,所以不能直接简单的停止线程?如何设计可以随时被中断而取消的任务线程?
2 为什么不能简单的停止(Stop())一个线程?
答:因为停止stop()会直接把线程停止,这样就没有给线程足够的时间来处理想要在停止前保存数据的逻辑,任务戛然而止,会导致出现数据完整性等问题。
如下图:Thread1被停止stop()后,立即释放内存锁;Thread3获得内存锁,马上加锁。Thread1根本没有清理内存的机会,原来写数据写到一半,现在没有机会继续写了,因为线程被杀掉了;接着,Thread3获得了时间片,开始读数据时发现内存状态异常,读到一个莫名其妙的数据,因为Thread1还没清理干净就停止线程了,留下一个烂摊子给Thread3,Thread3也会面临crash。所以,这样的停止操作是非常危险的。也正是这个原因,无论什么语言都把停止的api废弃了。
2.1 为什么暂停和继续(suspend()和resume())也被废弃了?
答:因为对于暂停suspend()和继续resume(),它们的问题在于如果线程调用suspend(),它并不会释放锁,就开始进入休眠,但此时有可能仍持有锁,这样就容易导致死锁问题,因为这把锁在线程被resume()之前,是不会被释放的。
如下图:Thread1调用暂停suspend()后进入休眠,但是仍持有内存锁;Thread3拿不到锁,会陷入阻塞,等待Thread1持有的内存锁释放。如果Thread1一直持有锁,Thread3一直在等待,就会出现死锁的情况。所以线程suspend()和resume()方法也被废弃了。
2.2 什么是协作的任务执行模式?
答:虽然线程不能在中间被停止/干掉,但是任务是可以停止的;想让线程结束的目的是让任务结束,而不是强制线程结束。
线程在设计过程中,主要是任务执行模式的设计,线程和任务是强绑定的,任务执行完了,线程也就结束了。虽然线程不能在中间被停止/干掉,但是任务是可以停止的,所以任务的执行模式是协作的任务执行模式。我们想让线程结束的目的是让任务结束,而不是强制线程结束。线程被停止和线程自己运行完,对开发者来说是没区别,但是对程序是有明显区别的。线程自己正常运行完,可以正常清理它创造的烂摊子;如果它被干掉了,它的烂摊子就不能被清理了,只能留给其他线程。所以,我们在设计上,应该是在任务上添加停止/结束的逻辑,而不是在线程上添加。我们有两种方式结束任务,分别是:Interrupt和boolean标志位。
3 如何使用线程中断机制-interrupt停止线程?
答:(1)如果原生支持interrupt:sleep、wait等可以让线程进入阻塞的方法使线程休眠了,而处于休眠中的线程被中断,那么线程是可以感受到中断信号的,并且会抛出一个InterruptedException异常,同时清除中断信号,将中断标记位设置成false。
// interrupt的原生支持 -> Sleep()
public static void rawInterrupt() throws InterruptedException {
// 创建目标线程
Thread interruptRawThread = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 线程正在执行...");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println(Thread.currentThread().getName() + " 线程接收到中断信息,中断线程...");
}
}, "rawInterrupt");
// 启动目标线程
interruptRawThread.start();
// 中断通知
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("rawInterrupt 设置线程中断....");
interruptRawThread.interrupt();
// rawInterrupt 线程正在执行...
// rawInterrupt 设置线程中断....
// rawInterrupt 线程接收到中断信息,中断线程...
}
(2)非原生不支持interrupt。
// 不支持 interrupt()
public static void unInterrupt() {
// 创建目标线程
Thread unInterruptThread = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
if (i % 10000 == 0) {
System.out.println(Thread.currentThread().getName() + " 线程正在执行... " + i);
}
}
}, "unInterrupt");
// 启动目标线程
unInterruptThread.start();
// 中断通知
System.out.println("unInterrupt 设置线程中断....");
unInterruptThread.interrupt();
// unInterrupt 设置线程中断....
// unInterrupt 线程正在执行... 0
// unInterrupt 线程正在执行... 10000
// unInterrupt 线程正在执行... 20000
// unInterrupt 线程正在执行... 30000
// unInterrupt 线程正在执行... 40000
// unInterrupt 线程正在执行... 50000
// unInterrupt 线程正在执行... 60000
// unInterrupt 线程正在执行... 70000
// unInterrupt 线程正在执行... 80000
// unInterrupt 线程正在执行... 90000
}
(3)非原生如果支持interrupt,每执行一次任务,调用interrupted()或isInterrupted()询问一遍系统是否已经中断了,如果中断了系统会告诉我们已经中断了,然后可以实现中断的逻辑。
// 支持 interrupted()
public static void supportInterrupted() {
// 创建目标线程
Thread supInterruptThread = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
System.out.println(Thread.currentThread().getName() + " 线程正在执行... " + i);
// 判断当前线程是否中断,
if (interrupted()) {
System.out.println(Thread.currentThread().getName() + " 线程接收到中断信息,中断线程...");
break;
}
}
}, "supportInterrupted");
// 启动目标线程
supInterruptThread.start();
// 中断通知
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("supportInterrupted 设置线程中断....");
supInterruptThread.interrupt();
// supportInterrupted 设置线程中断....
// supportInterrupted 线程正在执行... 0
// supportInterrupted 线程接收到中断信息,中断线程...
}
// 支持 IsInterrupted()
public static void supportIsInterrupted() {
// 创建目标线程
Thread supInterruptThread = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
System.out.println(Thread.currentThread().getName() + " 线程正在执行... " + i + " isInterrupted():" + Thread.currentThread().isInterrupted());
// 判断该线程是否中断,
if (Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread().getName() + " 线程接收到中断信息,中断线程...");
break;
}
}
}, "supportIsInterrupted");
// 启动目标线程
supInterruptThread.start();
// 中断通知
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("supportIsInterrupted 设置线程中断....");
supInterruptThread.interrupt();
// supportIsInterrupted 线程正在执行... 0 isInterrupted():false
// supportIsInterrupted 线程正在执行... 1 isInterrupted():false
// supportIsInterrupted 线程正在执行... 2 isInterrupted():false
// supportIsInterrupted 设置线程中断....
// supportIsInterrupted 线程正在执行... 3 isInterrupted():true
// supportIsInterrupted 线程接收到中断信息,中断线程...
}
3.1 是否知道interrupt底层的原理?
答:在Native层加锁。然后判断interrupted_值是否等于true,是的直接返回,表示已经是中断状态了;否则,把interrupted_置为true,发出中断通知。实现原理和boolean标志位的实现逻辑很像。
(1)Java层用法
supInterruptThread.interrupt();
(2)Thread.java层的实现
// Thread.java
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}
private native void interrupt0(); // native方法
(3)Native层实现:MutexLock mu(self,*wait_ mutex_)指先加了锁。然后判断interrupted_值是否等于true,是的直接返回,表示已经是中断状态了;否则,把interrupted_置为true,发出中断通知。实现原理和boolean标志位的实现逻辑很像。
补充最新源码:/thread.cc# Interrupt()
3.2 interrupted()与isInterrupted()有什么区别?
答:在Java层,interrupted()是静态方法,获取当前线程的中断状态后并清空,重复调用后续返回false;isInterrupted()是非静态方法,获取线程对象对应线程的中断状态,不清空,可重复调用,中断清空前一直返回true。在Native层,interrupted()比isInterrupted()多调用了SetInterruptedLocked(false)方法,清空当前中断状态,其他的都一致。
(1)Java层的区别:
(2)Native层-Thread.cc的区别:
interrupted()是静态方法:先拿到JNI的环境JNIEnv,他里面有个成员叫self,self就是底层线程的对象,调用的是这个self对象的interrupted()方法;
isInterrupted()是非静态方法:对应一个Java线程的对象,先找到这个Java线程的对象对应的底层线程对象,再调用这个底层线程对象的isInterrupted()方法。
补充最新源码:/thread.cc# interrupted() + isInterrupted()
(3)Native层-thread.cc的区别
最核心区别:interrupted()比isInterrupted()多调用了SetInterruptedLocked(false)方法,清空当前中断状态,其他的都一致。
补充最新源码:/thread.cc# interrupted() + isInterrupted()
补充最新源码:/thread.cc# storeSequentiallyConsistent()
3.3 InterruptedException的两种最佳处理方式?
答:在实际开发中肯定是团队协作的,不同的人负责编写不同的方法,然后相互调用来实现整个业务的逻辑。
(1)可以在方法中使用 try/catch 或在方法签名中声明 throws InterruptedException
void subTas() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 在这里不处理该异常是非常不好的
}
}
void subTask2() throws InterruptedException {
Thread.sleep(1000);
}
(2)再次中断:如果这时手动添加中断信号,中断信号依然可以被捕捉到。这样后续执行的方法依然可以检测到这里发生过中断,可以做出相应的处理,整个线程可以正常退出。
private void reInterrupt() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
4 如何用volatile boolean标志位通知线程停止?
答:(1)线程中设置一个boolean标志位值为false,线程里不断读取这个boolean值,其他地方可以修改这个boolean值;
(2)但是,boolean标志位没加锁,其他线程修改boolean标志位为true,线程中不一定能看到
(3)为了保证内存可见性,给boolean标志位添加volatile保证可见性;当某一个线程修改boolean标志位为true,线程中能立刻看到
(4)案例
// 创建目标线程
static class BooleanThread extends Thread {
volatile boolean isStopped = false;
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
System.out.println("volatileBoolean 线程正在执行... " + i);
// 判断该线程boolean标志位是否有效
if (isStopped) {
System.out.println("volatileBoolean 线程接收到中断信息,中断线程...");
break;
}
}
}
}
// volatile boolean标志位
public static void volatileBoolean() {
BooleanThread booleanThread = new BooleanThread();
// 启动目标线程
booleanThread.start();
// 中断通知
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("volatileBoolean 设置线程中断....");
booleanThread.isStopped = true;
// volatileBoolean 设置线程中断....
// volatileBoolean 线程正在执行... 0
// volatileBoolean 线程接收到中断信息,中断线程...
}
4.1 为什么用boolean标志位的停止方法不一定准确的?
答:使用阻塞队列在线程中执行queue.put()时发生阻塞,在它被叫醒之前是没有办法进入下一次循环判断canceled的值的,所以在这种情况下用volatile不能让任务停下来。
public static void volatileBooleanNot() {
try {
ArrayBlockingQueue storage = new ArrayBlockingQueue(8);
Producer producer = new Producer(storage);
Thread producerThread = new Thread(producer);
producerThread.start();
Thread.sleep(500);
Consumer consumer = new Consumer(storage);
while (consumer.needMoreNums()) {
System.out.println("volatileBooleanNot " + consumer.storage.take() + "被消费了");
Thread.sleep(100);
}
System.out.println("volatileBooleanNot 消费者不需要更多数据了。");
// 一旦消费不需要更多数据了,我们应该让生产者也停下来,但是实际情况却停不下来
producer.canceled = true;
System.out.println("volatileBooleanNot " + producer.canceled);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 不能结束,不会打印:volatileBooleanNot 生产者结束运行
// volatileBooleanNot 0是50的倍数,被放到仓库中了。
// volatileBooleanNot 50是50的倍数,被放到仓库中了。
// volatileBooleanNot 100是50的倍数,被放到仓库中了。
// volatileBooleanNot 150是50的倍数,被放到仓库中了。
// volatileBooleanNot 0被消费了
// volatileBooleanNot 50被消费了
// volatileBooleanNot 消费者不需要更多数据了。
// volatileBooleanNot true
}
static class Producer implements Runnable {
public volatile boolean canceled = false;
public BlockingQueue storage;
public Producer(BlockingQueue storage) {
this.storage = storage;
}
@Override
public void run() {
int num = 0;
try {
while (num <= 100000 && !canceled) {
// 生产者在执行storage.put(num)时发生阻塞,在它被叫醒之前是没有办法进入下一次循环判断canceled的值的,所以在这种情况下用volatile不能让生产者停下来。
// 相反如果用interrupt语句来中断,即使生产者处于阻塞状态,仍然能够感受到中断信号,并做响应处理。
if (num % 50 == 0) {
storage.put(num);
System.out.println("volatileBooleanNot " + num + "是50的倍数,被放到仓库中了。");
}
num++;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("volatileBooleanNot 生产者结束运行");
}
}
}
static class Consumer {
BlockingQueue storage;
public Consumer(BlockingQueue storage) {
this.storage = storage;
}
public boolean needMoreNums() {
if (Math.random() > 0.97) {
return false;
}
return true;
}
}
5 如何选择interrupt和boolean标志位去停止线程?
答:interrupt()和boolean标志位的原理是一致的。除非是用到了系统方法时(如:sleep) 或者 使用阻塞队列在线程中执行put()时发生阻塞,使用interrupt();否则,建议使用boolean标志位,性能更优,毕竟interrupt使用JNI有一定的开销。